//@author A0102127Y
#include "CmdParser.h"
using namespace Twentyfifth;
using namespace std;

//@author A0101119Y
const regex Parser::REGEX_DATE("(\\d{1,2})[.\\/-](\\d{1,2})[.\\/-](\\d{4})$");
const regex Parser::REGEX_TIME_LONG("(\\d{1,2})[:.]{0,1}(\\d{2})([apAP]).{0,3}$");
const regex Parser::REGEX_TIME_NO_MINUTES("(\\d{1,2})([apAP]).{0,3}$");
const regex Parser::REGEX_TIME_24HR("(\\d{1,2})(\\d{2})$");
const regex Parser::REGEX_TIME_24HR_NO_MINUTES("(\\d{1,2})$");
const string Parser::SUFFIX_PM = "p";
const string Parser::SUFFIX_AM = "a";

//@author A0101712B
Cmd::CmdType Parser::identifyCmdType(string s) {
	s = convertToLowerCase(s);
	if (s == CMD_ADD) {
		return Cmd::ADD;
	} else if (s == CMD_EDIT) {
		return Cmd::EDIT;
	} else if (s == CMD_VIEW) {
		return Cmd::VIEW;
	} else if (s == CMD_SEARCH) {
		return Cmd::SEARCH;
	} else if (s == CMD_DELETE) {
		return Cmd::DEL;
	} else if (s == CMD_UNDO) {
		return Cmd::UNDO;
	} else if (s == CMD_MARK) {
		return Cmd::MARK;
	} else if (s == CMD_GCAL) {
		return Cmd::GCAL;
	} else if (s == CMD_EXIT) {
		return Cmd::EXIT;
	} else {
		return Cmd::INVALID;
	}
}

//@author A0101712B
bool Parser::isAllDigit(string input) {
	for (unsigned int i = CONST_ZERO; i < input.length(); i++) {
		if (!isdigit(input[i])) {
			return false;
		}
	}
	return true;
}

//@author A0101712B
bool Parser::isAllAlpha(string input) {
	for (unsigned int i = CONST_ZERO; i < input.length(); i++) {
		if (!isalpha(input[i])) {
			return false;
		}
	}
	return true;
}

//@author A0101712B
string Parser::convertToLowerCase(string input) {
	for (unsigned int i = CONST_ZERO; i < input.length(); i++) {
		input[i] = tolower(input[i]);
	}
	return input;
}

//@author A0102127Y
struct tm Parser::getCurrTime() {
	struct tm newtime;
	__time64_t long_time;
	errno_t err;
	// Get time as 64-bit integer.
	_time64(&long_time);
	// Convert to local time.
	err = _localtime64_s(&newtime, &long_time);
	if (err) {
		printf(INVALID_LOCALTIME.c_str());
		exit(1);
	}
	return newtime;
}

//@author A0102127Y
int Parser::getDigitsCount(string input) {
	int counter = CONST_ZERO;
	for (unsigned int i = CONST_ZERO; i < input.length(); i++) {
		if (isdigit(input[i])) {
			counter++;
		}
	}
	return counter;
}

//@author A0102127Y
int Parser::getDayOfTheWeek(string input) {
	input = convertToLowerCase(input);
	if ((input == SUNDAY_LONG) || (input == SUNDAY_SHORT)) {
		return DAY_WEEK_FIRST;
	} else if ((input == MONDAY_LONG) || (input == MONDAY_SHORT)) {
		return DAY_WEEK_SECOND;
	} else if ((input == TUESDAY_LONG) || (input == TUESDAY_SHORT)) {
		return DAY_WEEK_THIRD;
	} else if ((input == WEDNESDAY_LONG) || (input == WEDNESDAY_SHORT)) {
		return DAY_WEEK_FOURTH;
	} else if ((input == THURSDAY_LONG) || (input == THURSDAY_SHORT)) {
		return DAY_WEEK_FIFTH;
	} else if ((input == FRIDAY_LONG) || (input == FRIDAY_SHORT)) {
		return DAY_WEEK_SIXTH;
	} else if ((input == SATURDAY_LONG) || (input == SATURDAY_SHORT)) {
		return DAY_WEEK_SEVENTH;
	}
	return DAY_INVALID;
}

//@author A0102127Y
vector<int> Parser::timeParse(string time) {
	time = convertToLowerCase(time);
	int hour;
	int minute;
	string suffix = BLANK_STRING;
	smatch match;
	if (regex_search(time, match, REGEX_TIME_LONG)) {
		hour = stoi(match[1]);
		minute = stoi(match[2]);
		suffix = convertToLowerCase(match[3]);
	} else if (regex_search(time, match, REGEX_TIME_NO_MINUTES)) {
		hour = stoi(match[1]);
		minute = 0;
		suffix = convertToLowerCase(match[2]);
	} else if (regex_search(time, match, REGEX_TIME_24HR)) {
		hour = stoi(match[1]);
		minute = stoi(match[2]);
	} else if (regex_search(time, match, REGEX_TIME_24HR_NO_MINUTES)) {
		hour = stoi(match[1]);
		minute = 0;
	}
	if (suffix == SUFFIX_AM) {
		if (hour >= 12) {
			hour %= 12;
		}
	} else if (suffix == SUFFIX_PM) {
		if (hour < 12) {
			hour += 12;
		}
	}

	vector<int> timeResult;
	timeResult.push_back(hour);
	timeResult.push_back(minute);
	return timeResult;
}

//@author A0102127Y
vector<int> Parser::dayParse(string date) {
	date = convertToLowerCase(date);
	int day;
	int month;
	int year;

	struct tm newtime = getCurrTime();

	if ((date == TODAY_LONG) || (date == TODAY_SHORT)) {
		//do nothing
	} else if ((date == TMR_LONG) || (date == TMR_SHORT)) {
		newtime.tm_mday += DAY_NEXT;
	} else {
		int dayOfWeek = getDayOfTheWeek(date);
		if (dayOfWeek != -1) {
			int currDayOfWeek = newtime.tm_wday;
			if (dayOfWeek < currDayOfWeek) {
				dayOfWeek += DAY_NEXT_WEEK;
			}
			int diff = dayOfWeek - currDayOfWeek;
			newtime.tm_mday += diff;
		}
	}

	struct tm* tempTime = &newtime;
	mktime(tempTime);
	newtime = *tempTime;

	day = newtime.tm_mday;
	month = newtime.tm_mon + DEFAULT_CMONTH_CONST;
	year = newtime.tm_year + DEFAULT_CYEAR_CONST;

	vector<int> dateResult;
	dateResult.push_back(day);
	dateResult.push_back(month);
	dateResult.push_back(year);
	return dateResult;
}

//@author A0102127Y
vector<int> Parser::dateParse(string date) {
	vector<int> dateResult;
	string dd;
	string mm;
	string yyyy;
	smatch match;
	int day;
	int month;
	int year;
	if (isAllAlpha(date)) {
		dateResult = dayParse(date);
		return dateResult;
	} else if (regex_search(date, match, REGEX_DATE)) {
		day = stoi(match[1]);
		month = stoi(match[2]);
		year = stoi(match[3]);
	}
	dateResult.push_back(day);
	dateResult.push_back(month);
	dateResult.push_back(year);
	return dateResult;
}

//@author A0102127Y
DateTime Parser::dateAndTimeParse(string time, string date) {

	time = convertToLowerCase(time);
	date = convertToLowerCase(date);

	if (date.length() == CONST_ZERO) {
		int numDigits = getDigitsCount(time);
		if ((numDigits > DEFAULT_TIME_LENGTH) || (numDigits == CONST_ZERO)) {
			swap(time, date);
			time = TIME_DEFAULT;
		}
	}

	int numDigits = getDigitsCount(date);
	if ((numDigits > CONST_ZERO) && (numDigits <= DEFAULT_TIME_LENGTH)) {
		swap(time, date);
	}

	//get Time
	vector<int> timeResult = timeParse(time);
	int hour = timeResult[INDEX_ZERO];
	int minute = timeResult[INDEX_FIRST];

	//get Date
	vector<int> dateResult = dateParse(date);
	int day = dateResult[INDEX_ZERO];
	int month = dateResult[INDEX_FIRST];
	int year = dateResult[INDEX_SECOND];

	DateTime start(hour, minute, day, month, year);
	return start;
}

//@author A0102127Y
void Parser::startParse(string input, Task &result) {
	istringstream istream(input);
	result.setTaskType(TYPE_TIMED_TASK);
	string time;
	string date;
	getline(istream, time, DELIMITER_SPACE);
	getline(istream, date, DELIMITER_SPACE);
	DateTime startTime = dateAndTimeParse(time, date);
	result.setStart(startTime);
	return;
}

//@author A0102127Y
void Parser::endParse(string input, Task &result) {
	DateTime start = result.getStart();
	if ((start.getHour() == DEFAULT_INVALID_TIME) && (start.getMinute() == DEFAULT_INVALID_TIME)) {
		result.setTaskType(TYPE_INVALID);
		return;
	}

	istringstream istream(input);
	result.setTaskType(TYPE_TIMED_TASK);
	string time;
	string date;
	getline(istream, time, DELIMITER_SPACE);
	getline(istream, date, DELIMITER_SPACE);
	DateTime endTime = dateAndTimeParse(time, date);

	int startInt = start.getYear() * CONST_ONE_THOUSAND + start.getMonth() * CONST_ONE_HUNDRED + start.getDay();
	int endInt = endTime.getYear() * CONST_ONE_THOUSAND + endTime.getMonth() * CONST_ONE_HUNDRED + endTime.getDay();

	if (startInt > endInt) {
		DateTime newEnd(endTime.getHour(), endTime.getMinute(), start.getDay(), start.getMonth(), start.getYear());
		result.setEnd(newEnd);
	} else {
		result.setEnd(endTime);
	}
	return;
}

//@author A0102127Y
void Parser::checkAndSetEndParse(Task &result) {
	DateTime start = result.getStart();
	DateTime end = result.getEnd();
	if ((start.getHour() == DEFAULT_INVALID_TIME) && (start.getMinute() == DEFAULT_INVALID_TIME)) {
		return;
	} else if ((end.getHour() == DEFAULT_INVALID_TIME) && (end.getMinute() == DEFAULT_INVALID_TIME)) {

		struct tm newtime = getCurrTime();

		int hour = start.getHour() + TIME_NEXT_HOUR;
		int min = start.getMinute();
		int day = start.getDay();
		int month = start.getMonth();
		int year = start.getYear();
		newtime.tm_hour = hour;
		newtime.tm_min = min;
		newtime.tm_mday = day;
		newtime.tm_mon = month - DEFAULT_CMONTH_CONST;
		newtime.tm_year = year - DEFAULT_CYEAR_CONST;

		struct tm* temptime = &newtime;
		mktime(temptime);
		newtime = *temptime;

		hour = newtime.tm_hour;
		min = newtime.tm_min;
		day = newtime.tm_mday;
		month = newtime.tm_mon + DEFAULT_CMONTH_CONST;
		year = newtime.tm_year + DEFAULT_CYEAR_CONST;

		DateTime newEnd(hour, min, day, month, year);
		result.setEnd(newEnd);
		return;
	}
}

//@author A0101712B
void Parser::deadlineParse(string input, Task &result) {
	istringstream istream(input);
	result.setTaskType(TYPE_DEADLINE_TASK);
	string time;
	string date;
	getline(istream, time, DELIMITER_SPACE);
	getline(istream, date, DELIMITER_SPACE);
	DateTime deadlineTime = dateAndTimeParse(time, date);
	result.setDeadline(deadlineTime);
	return;
}

//@author A0101712B
void Parser::venueParse(string word, Task &result) {
	int wordLength = word.length();
	if (word.substr(wordLength - 1) == STRING_SINGLE_SPACE) {
		word = word.substr(CONST_ZERO, wordLength - INDEX_FIRST);
	}
	result.setVenue(word);
	return;
}

//@author A0101712B
void Parser::descriptionParse(string word, Task &result) {
	int wordLength = word.length();
	if (word.substr(wordLength - INDEX_FIRST) == STRING_SINGLE_SPACE) {
		word = word.substr(CONST_ZERO, wordLength - INDEX_FIRST);
	}
	result.setDescription(word);
	return;
}

//@author A0102127Y
void Parser::optionParse(string input, string optionType, Task &result) {
	istringstream istream(input);
	string word;
	if (optionType == ATTRIB_ADD_START) {
		getline(istream, word, DELIMITER_DASH);
		startParse(word, result);
		getline(istream, word, DELIMITER_SPACE);
	} else if (optionType == ATTRIB_ADD_END) {
		getline(istream, word, DELIMITER_DASH);
		endParse(word, result);
		getline(istream, word, DELIMITER_SPACE);
	} else if (optionType == ATTRIB_ADD_DEADLINE) {
		getline(istream, word, DELIMITER_DASH);
		deadlineParse(word, result);
		getline(istream, word, DELIMITER_SPACE);
	} else if (optionType == ATTRIB_ADD_VENUE) {
		getline(istream, word, DELIMITER_DASH);
		venueParse(word, result);
		getline(istream, word, DELIMITER_SPACE);
	}
}

//@author A0102127Y
CmdAdd Parser::addParse(istringstream &istream) {
	Task result;
	string word;
	string optionType;
	if (!getline(istream, word, DELIMITER_DASH)) {
		CmdAdd newCmd;
		return newCmd;
	}
	int wordLength = word.length();
	if (word.substr(wordLength - INDEX_FIRST) == STRING_SINGLE_SPACE) {
		word = word.substr(CONST_ZERO, wordLength - INDEX_FIRST);
	}
	result.setDescription(word);
	if (!getline(istream, optionType, DELIMITER_SPACE)) {
		result.setTaskType(TYPE_FLOATING_TASK);
	} else {
		getline(istream, word, DELIMITER_DASH);
		optionParse(word, optionType, result);
	}
	while (getline(istream, optionType, DELIMITER_SPACE)) {
		getline(istream, word, DELIMITER_DASH);
		optionParse(word, optionType, result);
	}

	checkAndSetEndParse(result);

	CmdAdd newCmd;
	newCmd.setTask(result);
	return newCmd;
}

//@author A0102127Y
CmdDel Parser::deleteParse(istringstream &istream) {
	Task result;
	result.setTaskType(TYPE_QUERY);
	string word;
	int id;
	getline(istream, word);
	if (!isAllDigit(word)) {
		result.setTaskType(TYPE_INVALID);
	} else {
		id = atoi(word.c_str());
		result.setId(id);
	}
	CmdDel newCmd;
	newCmd.setTask(result);
	return newCmd;
}

//@author A0102127Y
CmdMark Parser::markParse(istringstream &istream) {
	Task result;
	result.setTaskType(TYPE_QUERY);
	string word;
	int id;
	CmdMark newCmd;
	getline(istream, word, DELIMITER_SPACE);
	if (!isAllDigit(word)) {
		result.setTaskType(TYPE_INVALID);
	} else {
		id = atoi(word.c_str());
		result.setId(id);
	}
	newCmd.setTask(result);
	return newCmd;
}

//@author A0102127Y
CmdView Parser::viewParse(istringstream &istream) {
	CmdView newCmd;

	Task result;
	result.setTaskType(TYPE_QUERY);
	string word;
	getline(istream, word);
	int numDigits = getDigitsCount(word);
	if ((numDigits > CONST_ZERO) && (numDigits<MAX_NUM_DIGIT) && isAllDigit(word)) {
		int id = atoi(word.c_str());
		result.setId(id);
	} else if (word.length()>CONST_ZERO) {
		vector<int> dateResult = dateParse(word);
		DateTime date(dateResult[INDEX_ZERO], dateResult[INDEX_FIRST], dateResult[INDEX_SECOND]);
		result.setStart(date);
	}
	newCmd.setTask(result);
	return newCmd;
}

//@author A0101712B
CmdEdit Parser::editParse(istringstream &istream) {
	Task result;
	string word;
	CmdEdit newCmd;
	int id;
	getline(istream, word, DELIMITER_SPACE);
	if (!isAllDigit(word)) {
		//throw error here, to be implemented later
		cout << INVALID_TASKID << endl;
	} else {//to set the id, so that storage knows which task to edit.
		istringstream convert(word);
		convert >> id;
		result.setId(id);
	}
	string word1, word2;
	while (getline(istream, word1, DELIMITER_SPACE)) {
		if (word1.substr(INDEX_ZERO, INDEX_FIRST) != STRING_SINGLE_DASH) {
			word1 = DELIMITER_DASH + word1;
		}
		if (word1 == ATTRIB_EDIT_STARTDATE) {
			getline(istream, word2, DELIMITER_SPACE);
			result.setTaskType(TYPE_TIMED_TASK);
			vector<int> dateResult = dateParse(word2);
			if (newCmd.startTimeEdited == false) {
				DateTime startDate(dateResult[INDEX_ZERO], dateResult[INDEX_FIRST], dateResult[INDEX_SECOND]);
				result.setStart(startDate);
			} else {
				int hour = result.getStart().getHour();
				int minute = result.getStart().getMinute();
				DateTime startDate(hour, minute, dateResult[INDEX_ZERO], dateResult[INDEX_FIRST], dateResult[INDEX_SECOND]);
				result.setStart(startDate);
			}
			newCmd.startDateEdited = true;
		} else if (word1 == ATTRIB_EDIT_ENDDATE) {
			getline(istream, word2, DELIMITER_SPACE);
			result.setTaskType(TYPE_TIMED_TASK);
			vector<int> dateResult = dateParse(word2);
			if (newCmd.endTimeEdited == false) {
				DateTime endDate(dateResult[INDEX_ZERO], dateResult[INDEX_FIRST], dateResult[INDEX_SECOND]);
				result.setEnd(endDate);
			} else {
				int hour = result.getEnd().getHour();
				int minute = result.getEnd().getMinute();
				DateTime endDate(hour, minute, dateResult[INDEX_ZERO], dateResult[INDEX_FIRST], dateResult[INDEX_SECOND]);
				result.setEnd(endDate);
			}
			newCmd.endDateEdited = true;
		} else if (word1 == ATTRIB_EDIT_STARTTIME) {
			getline(istream, word2, DELIMITER_SPACE);
			result.setTaskType(TYPE_TIMED_TASK);
			vector<int> timeResult = timeParse(word2);
			if (newCmd.startDateEdited == false) {
				DateTime startTime(timeResult[INDEX_ZERO], timeResult[INDEX_FIRST]);
				result.setStart(startTime);
			} else {
				int day = result.getStart().getDay();
				int month = result.getStart().getMonth();
				int year = result.getStart().getYear();
				DateTime startDate(timeResult[INDEX_ZERO], timeResult[INDEX_FIRST], day, month, year);
				result.setStart(startDate);
			}
			newCmd.startTimeEdited = true;
		} else if (word1 == ATTRIB_EDIT_ENDTIME) {
			getline(istream, word2, DELIMITER_SPACE);
			result.setTaskType(TYPE_TIMED_TASK);
			vector<int> timeResult = timeParse(word2);
			if (newCmd.endDateEdited == false) {
				DateTime endTime(timeResult[INDEX_ZERO], timeResult[INDEX_FIRST]);
				result.setEnd(endTime);
			} else {
				int day = result.getEnd().getDay();
				int month = result.getEnd().getMonth();
				int year = result.getEnd().getYear();
				DateTime endDate(timeResult[INDEX_ZERO], timeResult[INDEX_FIRST], day, month, year);
				result.setEnd(endDate);
			}
			newCmd.endTimeEdited = true;
		} else if (word1 == ATTRIB_EDIT_DEADLINEDATE) {
			getline(istream, word2, DELIMITER_SPACE);
			result.setTaskType(TYPE_DEADLINE_TASK);
			vector<int> dateResult = dateParse(word2);
			if (newCmd.deadlineTimeEdited == false) {
				DateTime deadlineDate(dateResult[INDEX_ZERO], dateResult[INDEX_FIRST], dateResult[INDEX_SECOND]);
				result.setDeadline(deadlineDate);
			} else {
				int hour = result.getDeadline().getHour();
				int minute = result.getDeadline().getMinute();
				DateTime deadlineDate(hour, minute, dateResult[INDEX_ZERO], dateResult[INDEX_FIRST], dateResult[INDEX_SECOND]);
				result.setDeadline(deadlineDate);
			}
			newCmd.deadlineDateEdited = true;
		} else if (word1 == ATTRIB_EDIT_DEADLINETIME) {
			getline(istream, word2, DELIMITER_SPACE);
			result.setTaskType(TYPE_DEADLINE_TASK);
			vector<int> timeResult = timeParse(word2);
			if (newCmd.deadlineDateEdited == false) {
				DateTime deadlineTime(timeResult[INDEX_ZERO], timeResult[INDEX_FIRST]);
				result.setDeadline(deadlineTime);
			} else {
				int day = result.getDeadline().getDay();
				int month = result.getDeadline().getMonth();
				int year = result.getDeadline().getYear();
				DateTime deadlineDate(timeResult[INDEX_ZERO], timeResult[INDEX_FIRST], day, month, year);
				result.setDeadline(deadlineDate);
			}
			newCmd.deadlineTimeEdited = true;
		} else if (word1 == ATTRIB_DESCRIPTION) {
			getline(istream, word, DELIMITER_DASH);
			descriptionParse(word, result);
			newCmd.descEdited = true;
		} else if (word1 == ATTRIB_VENUE) {
			getline(istream, word, DELIMITER_DASH);
			venueParse(word, result);
			newCmd.venueEdited = true;
		}
	}
	newCmd.setTask(result);
	return newCmd;
}

//@author A0101712B
CmdSearch Parser::searchParse(istringstream &istream) {
	CmdSearch newCmd;
	smatch match;
	Task result;
	result.setTaskType(TYPE_QUERY);
	string word;
	getline(istream, word);
	if ((regex_search(word, match, REGEX_DATE)) || word == TODAY_LONG || word == TODAY_SHORT || word == TMR_LONG || word == TMR_SHORT) {
		vector<int> searchDateMembers = dateParse(word);
		int day = searchDateMembers[0];
		int month = searchDateMembers[1];
		int year = searchDateMembers[2];
		DateTime searchDate;
		if (searchDate.isValidDate(day, month, year)) {
			searchDate.setDay(day);
			searchDate.setMonth(month);
			searchDate.setYear(year);
			newCmd.searchDate = searchDate;
		}
	} else {
		result.setDescription(word);
		newCmd.setTask(result);
	}
	return newCmd;
}

//@author A0102127Y
CmdGCal Parser::gcalParse(istringstream &istream) {
	string info;
	getline(istream, info);
	CmdGCal newCmd;
	newCmd.setCommand(info);
	return newCmd;
}


//@author A0102127Y
Cmd* Parser::parse(string userInput) {
	Cmd result;

	string cmdType = getFirstWord(userInput);
	Cmd::CmdType type = identifyCmdType(cmdType);
	Cmd* resultPointer;

	istringstream istream(removeFirstWord(userInput));
	switch (type) {
	case Cmd::ADD:
		resultPointer = new CmdAdd();
		result = addParse(istream);
		if (result.getTask().getTaskType() == Task::INVALID) {
			return new CmdInvalid();
		}
		break;
	case Cmd::DEL:
		resultPointer = new CmdDel();
		result = deleteParse(istream);
		break;
	case Cmd::MARK:
		resultPointer = new CmdMark();
		result = markParse(istream);
		break;
	case Cmd::EDIT:
		resultPointer = new CmdEdit();
		result = editParse(istream);
		break;
	case Cmd::VIEW:
		resultPointer = new CmdView();
		result = viewParse(istream);
		break;
	case Cmd::SEARCH:
		resultPointer = new CmdSearch();
		result = searchParse(istream);
		break;
	case Cmd::GCAL:
		resultPointer = new CmdGCal();
		result = gcalParse(istream);

		break;
	case Cmd::EXIT:
		resultPointer = new CmdExit();
		result = *resultPointer;
		break;
	case Cmd::UNDO:
		resultPointer = new CmdUndo();
		result = *resultPointer;
		break;
	default:
		resultPointer = new CmdInvalid();
		result = *resultPointer;
		break;
	}
	*resultPointer = result;
	return resultPointer;
}

//@author A0102127Y
string Parser::getFirstWord(string s) {
	int spacePosition = getSpaceAfterFirstWord(s);
	return s.substr(CONST_ZERO, spacePosition);
}

//@author A0102127Y
string Parser::removeFirstWord(string s) {
	int spacePosition = getSpaceAfterFirstWord(s);
	if (spacePosition == s.length()) {
		return BLANK_STRING;
	} else {
		return s.substr(spacePosition + INDEX_FIRST, string::npos);
	}
}

//@author A0102127Y
int Parser::getSpaceAfterFirstWord(string s) {
	int pos = CONST_ZERO;
	while (s[pos] != DELIMITER_ENDCHAR) {
		if (s[pos] == DELIMITER_SPACE) {
			return pos;
		}
		pos++;
	}
	return pos;
}